package uk.co.droidinactu.common;

public class Base64 {

	// *****************************************************************************
	// INSTANCE PROPERTIES
	// *****************************************************************************

	private String lineSeparator;
	private int lineLength;

	// *****************************************************************************
	// INSTANCE CONSTRUCTION/INITIALIZATON/FINALIZATION, OPEN/CLOSE
	// *****************************************************************************

	static private final char[] ENCODE = new char[64]; // translation
	                                                   // array for
	// encoding

	// *****************************************************************************
	// INSTANCE METHODS
	// *****************************************************************************

	static private final int[] DECODE = new int[256]; // translation array for
	                                                  // decoding
	static private final int IGNORE = -1; // flag for values to ignore when
	                                      // decoding

	static private final int PAD = -2; // flag value for padding value when
	                                   // decoding

	static private final Base64 BASE64 = new Base64(); // default converter

	static {
		for (int xa = 0; xa <= 25; xa++) {
			ENCODE[xa] = (char) ('A' + xa);
		} // 0..25 -> 'A'..'Z'
		for (int xa = 0; xa <= 25; xa++) {
			ENCODE[xa + 26] = (char) ('a' + xa);
		} // 26..51 -> 'a'..'z'
		for (int xa = 0; xa <= 9; xa++) {
			ENCODE[xa + 52] = (char) ('0' + xa);
		} // 52..61 -> '0'..'9'
		ENCODE[62] = '+';
		ENCODE[63] = '/';

		for (int xa = 0; xa < 256; xa++) {
			DECODE[xa] = IGNORE;
		} // set all chars to IGNORE, first
		for (int xa = 0; xa < 64; xa++) {
			DECODE[ENCODE[xa]] = xa;
		} // set the Base 64 chars to their integer byte values
		DECODE['='] = PAD;
	}

	static public byte[] toBytes(final String b64) {
		return BASE64.decode(b64, 0, b64.length());
	}

	// *****************************************************************************
	// STATIC PROPERTIES
	// *****************************************************************************

	static public byte[] toBytes(final String b64, final int str, final int len) {
		return BASE64.decode(b64, str, len);
	}

	static public String toString(final byte[] dta) {
		return BASE64.encode(dta);
	}

	static public String toString(final byte[] dta, final int str, final int len) {
		return BASE64.encode(dta, str, len);
	}

	public Base64() {
		this.lineSeparator = System.getProperty("line.separator");
		this.lineLength = 0;
	}

	public byte[] decode(final String b64) {
		return this.decode(b64, 0, b64.length());
	}

	// *****************************************************************************
	// STATIC INIT & MAIN
	// *****************************************************************************

	/**
	 * Decode a Base64 string to an array of bytes. The string must have a
	 * length evenly divisible by 4 (not counting line separators and other
	 * ignorable characters, like whitespace).
	 */
	public byte[] decode(final String b64, final int str, final int len) {
		byte[] ba; // target byte array
		int dc; // decode cycle (within sequence of 4 input chars).
		int rv; // reconstituted value
		int ol; // output length
		int pc; // padding count

		ba = new byte[(len / 4) * 3]; // create array to largest possible size.
		dc = 0;
		rv = 0;
		ol = 0;
		pc = 0;

		for (int xa = 0; xa < len; xa++) {
			final int ch = b64.charAt(xa + str);
			int value = (ch <= 255 ? DECODE[ch] : IGNORE);
			if (value != IGNORE) {
				if (value == PAD) {
					value = 0;
					pc++;
				}
				switch (dc) {
				case 0: {
					rv = value;
					dc = 1;
				}
					break;

				case 1: {
					rv <<= 6;
					rv |= value;
					dc = 2;
				}
					break;

				case 2: {
					rv <<= 6;
					rv |= value;
					dc = 3;
				}
					break;

				case 3: {
					rv <<= 6;
					rv |= value;

					// Completed a cycle of 4 chars, so recombine the four 6-bit
					// values in big-endian order
					ba[ol + 2] = (byte) rv;
					rv >>>= 8;
					ba[ol + 1] = (byte) rv;
					rv >>>= 8;
					ba[ol] = (byte) rv;
					ol += 3;
					dc = 0;
				}
					break;
				}
			}
		}
		if (dc != 0) {
			throw new ArrayIndexOutOfBoundsException(
			        "Base64 data given as input was not an even multiple of 4 characters (should be padded with '=' characters).");
		}
		ol -= pc;
		if (ba.length != ol) {
			final byte[] b2 = new byte[ol];
			System.arraycopy(ba, 0, b2, 0, ol);
			ba = b2;
		}
		return ba;
	}

	// *****************************************************************************
	// STATIC METHODS
	// *****************************************************************************

	public String encode(final byte[] bin) {
		return this.encode(bin, 0, bin.length);
	}

	/**
	 * Encode an array of bytes as Base64. It will be broken into lines if the
	 * line length is not 0. If broken into lines, the last line is not
	 * terminated with a line separator.
	 * 
	 * param ba The byte array to encode.
	 */
	public String encode(final byte[] bin, final int str, final int len) {
		int ol; // output length
		StringBuffer sb; // string buffer for output(must be local for recursion
		                 // to work).
		int lp; // line position(must be local for recursion to work).
		int el; // even multiple of 3 length
		int ll; // leftover length

		// CREATE OUTPUT BUFFER
		ol = (((len + 2) / 3) * 4);
		if (this.lineLength != 0) {
			final int lines = (((ol + this.lineLength - 1) / this.lineLength) - 1);
			if (lines > 0) {
				ol += (lines * this.lineSeparator.length());
			}
		}
		sb = new StringBuffer(ol);
		lp = 0;

		// EVEN MULTIPLES OF 3
		el = (len / 3) * 3;
		ll = (len - el);
		for (int xa = 0; xa < el; xa += 3) {
			int cv;
			int c0, c1, c2, c3;

			if (this.lineLength != 0) {
				lp += 4;
				if (lp > this.lineLength) {
					sb.append(this.lineSeparator);
					lp = 4;
				}
			}

			// get next three bytes in unsigned form lined up, in big-endian
			// order
			cv = (bin[xa + str + 0] & 0xFF);
			cv <<= 8;
			cv |= (bin[xa + str + 1] & 0xFF);
			cv <<= 8;
			cv |= (bin[xa + str + 2] & 0xFF);

			// break those 24 bits into a 4 groups of 6 bits, working LSB to
			// MSB.
			c3 = cv & 0x3F;
			cv >>>= 6;
			c2 = cv & 0x3F;
			cv >>>= 6;
			c1 = cv & 0x3F;
			cv >>>= 6;
			c0 = cv & 0x3F;

			// Translate into the equivalent alpha character emitting them in
			// big-endian order.
			sb.append(ENCODE[c0]);
			sb.append(ENCODE[c1]);
			sb.append(ENCODE[c2]);
			sb.append(ENCODE[c3]);
		}

		// LEFTOVERS
		if (this.lineLength != 0 && ll > 0) {
			lp += 4;
			if (lp > this.lineLength) {
				sb.append(this.lineSeparator);
				lp = 4;
			}
		}
		if (ll == 1) {
			sb.append(this.encode(new byte[] { bin[el + str], 0, 0 }).substring(0, 2)).append("=="); // Use
			                                                                                         // recursion
			                                                                                         // so
			                                                                                         // escaping
			                                                                                         // logic
			                                                                                         // is
			                                                                                         // not
			                                                                                         // repeated,
			                                                                                         // replacing
			                                                                                         // last
			                                                                                         // 2
			                                                                                         // chars
			                                                                                         // with
			                                                                                         // "==".
		} else if (ll == 2) {
			sb.append(this.encode(new byte[] { bin[el + str], bin[el + str + 1], 0 }).substring(0, 3)).append("="); // Use
			                                                                                                        // recursion
			                                                                                                        // so
			                                                                                                        // escaping
			                                                                                                        // logic
			                                                                                                        // is
			                                                                                                        // not
			                                                                                                        // repeated,
			                                                                                                        // replacing
			                                                                                                        // last
			                                                                                                        // char
			                                                                                                        // and
			                                                                                                        // "=".
		}
		if (ol != sb.length()) {
			throw new RuntimeException("Error in Base64 encoding method: Calculated output length of " + ol
			        + " did not match actual length of " + sb.length());
		}
		return sb.toString();
	}

	/**
	 * Set maximum line length for encoded lines. Ignored by decode.
	 * 
	 * @param len
	 *            Length of each line. 0 means no newlines inserted. Must be a
	 *            multiple of 4.
	 */
	public void setLineLength(final int len) {
		this.lineLength = (len / 4) * 4;
	}

	/**
	 * Set the line separator sequence for encoded lines. Ignored by decode.
	 * Usually contains only a combination of chars \n and \r, but could be any
	 * chars except 'A'-'Z', 'a'-'z', '0'-'9', '+' and '/'.
	 * 
	 * @param linsep
	 *            Line separator - may be "" but not null.
	 */
	public void setLineSeparator(final String linsep) {
		this.lineSeparator = linsep;
	}

} // END PUBLIC CLASS
